<?php

namespace Aspic\Service;

use Aspic as A;

/**
 * @author Mallory Dessaintes
 */
class ServiceManager {

	/**
	 * @var array 2-dim array('service', 'reflection', 'instance')
	 */
	protected $services;

	/**
	 *
	 * @var array Parameters that could be used by services as dependency
	 */
	protected $params;

	public function __construct() {
		$this->services = array();
	}

	/**
	 * Register a service
	 */
	public function registerService(Service $service, $serviceName = null) {
		$serviceNameResolved = ($serviceName)? : $service->getServiceName();

		$this->services[$serviceNameResolved]['service'] = $service;
		$this->services[$serviceNameResolved]['reflection'] = new \ReflectionClass($service->geClassName());
		$this->services[$serviceNameResolved]['instance'] = null;
	}

	public function registerServiceClass($className) {
		$ref = new \ReflectionClass($className);

		if ($ref->implementsInterface('Aspic\\Service\\ServiceInterface') == false) {
			throw new Exception(array('Class "?" must implements Aspic\\Service\\ServiceInterface', $className));
		}

		$serviceName = $ref->getMethod('getServiceName')->invoke(null);
		$type = $ref->getMethod('getServiceType')->invoke(null);
		$dependencies = $ref->getMethod('getServiceDependencies')->invoke(null);

		$service = new Service($className, $serviceName, $type, $dependencies);

		return $this->registerService($service);
	}

	public function setParam($name, $value) {
		$this->params[$name] = $value;
	}

	public function getParam($name) {
		return $this->params[$name];
	}

	/**
	 * @param Service[] $services
	 */
	public function registerServices(array $services) {
		foreach ($services as $service) {
			$this->registerService($service);
		}
	}

	/**
	 * Get instance of $serviceName
	 * @return ServiceInterface
	 */
	public function get($name) {
		if ($this->paramExists($name)) {
			return $this->getParam($name);
		}
		else {
			if (!$this->serviceExists($name)) {
				throw new A\Exception(array('Service: "?" is not registered', $name));
			}

			$service = $this->_get($name);

			if (!is_null($service['instance'])) {
				return $service['instance'];
			}
			else {
				$this->initService($name);

				return $this->get($name);
			}
		}
	}

	/**
	 * Return a service data that have already been insianciated
	 */
	protected function _get($serviceName) {
		return $this->services[$serviceName];
	}

	public function serviceExists($serviceName) {
		return isset($this->services[$serviceName]);
	}

	public function isServiceInitialized($serviceName) {
		return ($this->serviceExists($serviceName) AND $this->services[$serviceName]['object'] !== null);
	}

	protected function initService($serviceName) {
		// Use to avoid cycle in dependencies
		if (!$this->isServiceInitialized($serviceName)) {
			$serviceData = $this->_get($serviceName);
			$service = $serviceData['service'];
			/* @var $service Service */
			$reflection = $serviceData['reflection'];
			$dependencies = $service->getDependencies();

			// Init dependencies
			$this->initDependencies($dependencies);

			$constructorParams = array();
			$constructorDep = $service->getConstructorDependency();

			if ($constructorDep) {
				$constructorArgsNames = $constructorDep->servicesNames;

				foreach ($constructorArgsNames as $name) {
					// $name is a service name of param name
					$constructorParams[] = $this->get($name);
				}
			}

			$serviceInst = $reflection->newInstanceArgs($constructorParams);

			// Make calls and set properties to instance

			foreach ($service->getDependenciesByType(Dependency::TYPE_METHOD_CALL) as $dep) {
				$args = array();

				foreach ($dep->servicesNames as $serviceName) {
					$args[] = $this->get($serviceName);
				}

				call_user_func_array($serviceInst, $args);
			}

			// Set properties

			foreach ($service->getDependenciesByType(Dependency::TYPE_PROPERTY) as $dep) {
				$serviceInst->{$dep->callPropName} = $this->get($dep->servicesNames);
			}

			// We do not save the object if it must be re-created each time
			if ($serviceData['service']->getServiceType() == ServiceInterface::TYPE_SINGLETON) {
				$this->services[$serviceName]['instance'] = $serviceInst;
			}
		}
	}

	/**
	 *
	 * @param Dependency[] $dependencies
	 *
	 * @throws A\Exception
	 */
	protected function initDependencies(array $dependencies) {
		if (A\UArray::isNotEmptyArray($dependencies)) {
			foreach ($dependencies as $dependency) {
				/* @var $dependency Dependency */
				foreach ($dependency->servicesNames as $serviceName) {
					if ($this->paramExists($serviceName) == false) {
						if ($this->_get($serviceName)->getServiceType() == ServiceInterface::TYPE_INSTANCE) {
							throw new A\Exception('A service cannot have dependencies on "TYPE_INSTANCE" service');
						}

						$this->initService($serviceName);
					}
				}
			}
		}
	}

	public function paramExists($name) {
		return isset($this->params[$name]);
	}

}
